﻿using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.QueryParsers;
using Lucene.Net.Search;

namespace Lucene.Linq.Expressions
{
    internal class ParsingQueryTranslator : LuceneExpressionVisitor, IQueryTranslator
    {
        private readonly StringBuilder _queryString = new StringBuilder();
        private readonly List<SortField> _sortFields = new List<SortField>();


        private string[] _defaultFieldNames;

        internal StringBuilder QueryString
        {
            get { return _queryString; }
        }

        #region Constructors

        public ParsingQueryTranslator()
            : this(new StringBuilder())
        {
        }

        public ParsingQueryTranslator(StringBuilder queryString)
        {
            if (queryString == null) throw new ArgumentNullException("queryString");
            _queryString = queryString;
        }

        #endregion

        #region Translate

        public ISearchQuery Translate(Expression expression)
        {
            // visit the expression first to generate the query
            Visit(expression);
            return Translate(QueryString.ToString(), _defaultFieldNames);
        }

        internal ISearchQuery Translate(string query, string[] defaultFieldNames)
        {
            Query queryObject;


            if (defaultFieldNames.Length == 0)
            {
                throw new InvalidOperationException(
                    "Lucene Document-Classes must define at least one field as a default search field");
            }

            //Console.WriteLine("Lucene.Net Query for Parser : "+query);
            //TODO: use real document analyzer instead of just standard analyzer
            if (String.IsNullOrEmpty(query))
            {
                // If empty, build a match all query doc

                queryObject = new MatchAllDocsQuery();
            }
            else
            {
                // Build a parser and create query obj

                QueryParser parser = defaultFieldNames.Length == 1
                                         ? new QueryParser(defaultFieldNames[0], new StandardAnalyzer())
                                         : new MultiFieldQueryParser(defaultFieldNames, new StandardAnalyzer());
                queryObject = parser.Parse(query);
            }


            //
            // TODO: this sort should be used and not the one in QueryProvider.. not sure how to implement it
            //
            var sort = new Sort(_sortFields.ToArray());
            var searchQuery = new SearchQueryContainer(queryObject, sort);

            return searchQuery;
        }

        #endregion

        #region Private Methods

        private void VisitStartsWith(MethodCallExpression m)
        {
            string phrase;

            if (m.Object != null && m.Object.Type == typeof(string))
            {
                var field = (FieldExpression)m.Object;
                var constant = (ConstantExpression)m.Arguments[0];
                phrase = (string)constant.Value;

                int indexLength = phrase.Split(' ').Length > 1 ? 2 : 0;
                QueryString.Insert(QueryString.Length - phrase.Length - indexLength, field.Name + ":(");
                QueryString.Append("*)");
            }
            else
            {
                var constant = (ConstantExpression)m.Arguments[1];
                phrase = (string)constant.Value;

                QueryString.Append("(");
                Visit(m.Arguments[1]);
                QueryString.Append("*)");
            }

            // For multi-phrase queries, remove the quotation marks that would break it
            if (phrase.Split(' ').Length > 1)
            {
                QueryString.Remove(QueryString.ToString().LastIndexOf(phrase, StringComparison.Ordinal) - 1, 1); // Left Quotation Mark
                QueryString.Remove(QueryString.ToString().LastIndexOf(phrase, StringComparison.Ordinal) + phrase.Length, 1); // Right Quation Mark
            }
        }

        #endregion

        #region Visiting Expression Tree

        protected override Expression VisitMethodCall(MethodCallExpression m)
        {
            if (!(m.Arguments[0] is MemberInitExpression))
                Visit(m.Arguments[0]);

            if (QueryString.ToString().EndsWith(")"))
                QueryString.Append(" ");

            switch (m.Method.Name)
            {
                case "StartsWith":
                    VisitStartsWith(m);
                    break;
                case "Like":
                    Visit(m.Arguments[1]);
                    QueryString.Append("~");

                    if (m.Arguments.Count == 3)
                        QueryString.Append(m.Arguments[2]);

                    break;
                case "Match":
                    Visit(m.Arguments[1]);
                    break;
                case "Between":
                    QueryString.Append("{");
                    Visit(m.Arguments[1]);
                    QueryString.Append(" TO ");
                    Visit(m.Arguments[2]);
                    QueryString.Append("}");
                    break;
                case "Include":
                    QueryString.Append("[");
                    Visit(m.Arguments[1]);
                    QueryString.Append(" TO ");
                    Visit(m.Arguments[2]);
                    QueryString.Append("]");
                    break;
                case "Boost":
                    QueryString.Append("^");
                    QueryString.Append(m.Arguments[1]);
                    break;
                case "Search":
                    var constant = (ConstantExpression)m.Arguments[1];
                    var phrase = (string)constant.Value;

                    QueryString.Append(m.Arguments[1]);
                    QueryString.Remove(QueryString.ToString().LastIndexOf(phrase, StringComparison.Ordinal) - 1, 1); // Left Quotation Mark
                    QueryString.Remove(QueryString.ToString().LastIndexOf(phrase, StringComparison.Ordinal) + phrase.Length, 1);
                    // Right Quation Mark
                    break;
                default:
                    throw new NotSupportedException(string.Format(
                        "Method '{0}' has no supported translation to Lucene", m.Method.Name));
            }

            return m;
        }

        protected override Expression VisitUnary(UnaryExpression u)
        {
            switch (u.NodeType)
            {
                case ExpressionType.Not:
                    if (!QueryString.ToString().EndsWith("(") && !QueryString.ToString().EndsWith(" "))
                        QueryString.Append(" ");

                    QueryString.Append("-");
                    Visit(u.Operand);
                    break;
                default:
                    throw new NotSupportedException(string.Format("The unary operator '{0}' is not supported",
                                                                  u.NodeType));
            }

            return u;
        }

        protected override Expression VisitBinary(BinaryExpression b)
        {
            QueryString.Append("(");
            Visit(b.Left);
            switch (b.NodeType)
            {
                case ExpressionType.And:
                case ExpressionType.AndAlso:
                    QueryString.Append(" AND ");
                    break;
                case ExpressionType.Or:
                case ExpressionType.OrElse:
                    QueryString.Append(" OR ");
                    break;
                case ExpressionType.Equal:
                    break;
                case ExpressionType.Not:
                    QueryString.Append("NOT ");
                    break;
                case ExpressionType.NotEqual:
                    int index = QueryString.ToString().LastIndexOf(" ", StringComparison.Ordinal);
                    QueryString.Insert(index + 1, "-");
                    break;
                default:
                    throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported",
                                                                  b.NodeType));
            }
            Visit(b.Right);
            QueryString.Append(")");
            return b;
        }

        protected override Expression VisitConstant(ConstantExpression c)
        {
            switch (Type.GetTypeCode(c.Value.GetType()))
            {
                case TypeCode.String:
                    var phrase = c.Value as string;
                    if (phrase.Split(' ').Length > 1)
                        QueryString.Append("\"").Append(c.Value).Append("\"");
                    else
                        QueryString.Append(c.Value);
                    break;
                case TypeCode.Object:
                    var args = c.Value as string[];

                    if (args != null)
                    {
                        QueryString.Append("(");
                        foreach (string s in args)
                        {
                            if (s.Split(' ').Length > 1)
                            {
                                QueryString.Append("\"");

                                int index = s.LastIndexOf("^", StringComparison.Ordinal);
                                if (index != -1)
                                {
                                    string m = s.Insert(index, "\"");
                                    QueryString.Append(m);
                                }
                                else
                                {
                                    QueryString.Append(s).Append("\"");
                                }
                            }
                            else
                            {
                                QueryString.Append(s);
                            }

                            QueryString.Append(" ");
                        }

                        QueryString.Remove(QueryString.Length - 1, 1);
                        QueryString.Append(")");
                    }
                    break;
                case TypeCode.Boolean:
                    QueryString.Append("(");
                    QueryString.Append("\"");
                    QueryString.Append(((bool)c.Value) ? "1" : "0");
                    QueryString.Append("\"");
                    QueryString.Append(")");
                    break;

                case TypeCode.UInt16:
                case TypeCode.UInt32:
                case TypeCode.UInt64:
                case TypeCode.Int16:
                case TypeCode.Int32:
                case TypeCode.Int64:
                case TypeCode.Decimal:
                case TypeCode.Single:
                case TypeCode.Double:
                    QueryString.Append("(");
                    QueryString.Append("\"");
                    //TODO: add formatter support
                    QueryString.Append((c.Value).ToString());
                    QueryString.Append("\"");
                    QueryString.Append(")");
                    break;

                default:
                    throw new NotSupportedException(string.Format("The constant for '{0}' is not supported", c.Value));
                    break;
            }

            return c;
        }

        protected override Expression VisitOrderBy(OrderByExpression orderByExpression)
        {
            // TODO: add to sort field
            //QueryString.Append(field.Name).Append(":");
            // get sort field name
            // TODO: support multiple order bys
            throw new NotSupportedException();
        }

        protected override Expression VisitField(FieldExpression field)
        {
            QueryString.Append(field.Name).Append(":");
            return field;
        }

        protected override Expression VisitSelect(SelectExpression select)
        {
            if (select.DefaultFieldNames != null)
            {
                _defaultFieldNames = select.DefaultFieldNames;
            }

            if (select.From != null)
            {
                VisitSource(select.From);
            }

            if (select.Where != null)
            {
                Visit(select.Where);
            }

            return select;
        }

        protected override Expression VisitSource(Expression source)
        {
            switch ((LuceneExpressionType)source.NodeType)
            {
                case LuceneExpressionType.Index:
                    var index = (IndexExpression)source;
                    //sb.Append(index.Name);
                    //sb.Append(" AS ");
                    //sb.Append(index.Alias);
                    break;
                case LuceneExpressionType.Select:
                    var select = (SelectExpression)source;
                    //sb.Append("(");
                    Visit(select);
                    //sb.Append(")");
                    //sb.Append(" AS ");
                    //sb.Append(select.Alias);
                    break;


                default:
                    throw new InvalidOperationException("Select source is not valid type");
            }
            return source;
        }

        #endregion
    }
}